Skip to content

feat(orchestration-v2): attribute reused subagents to the run driving them (3/5) - #4662

Draft
shivamhwp wants to merge 3 commits into
subagent-obs/02-adaptersfrom
subagent-obs/03-reuse
Draft

feat(orchestration-v2): attribute reused subagents to the run driving them (3/5)#4662
shivamhwp wants to merge 3 commits into
subagent-obs/02-adaptersfrom
subagent-obs/03-reuse

Conversation

@shivamhwp

@shivamhwp shivamhwp commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Third of five stacked PRs replacing #4551. Stacked on #4629 — review that first.

A subagent row keeps the run id it was spawned under. Routing admits a row by run or by parent thread, and the parent thread is excluded by construction — so a later run re-activating that subagent could only see its updates while the spawning run's ingestion fiber happened to still be alive. The Claude adapter already re-attributed on reopen for exactly this reason; this brings the same treatment to routing and to Codex.

The bug that made this invisible

The rebind was hooked to item/started. Codex's reuse frames — resumeAgent, sendInput — only ever arrive on item/completed, and registerSubagentThreads ignores them because they aren't spawnAgent. So the rebind never fired, and I could not reproduce the fix working.

Moving the hook is what makes it observable. subagent_continue now asserts the subagent is attributed to the run that drove it:

subagent.runId
before ordinal:1 (spawning run)
after ordinal:2 (run that drove the reuse)

Routing

Identity becomes a third ownership test, seeded from the projection at turn start. Two constraints shape it:

  • The child thread is adopted only while the agent is live. A settled row is also re-emitted by trailing traffic — a token-usage frame, a collab state sweep — and adopting on those would hand a later run an interrupted agent's thread, readmitting exactly the stale events that post-interrupt recovery exists to exclude.
  • A subagent's row and its timeline item are emitted together, so both route by identity. Matching only the row advanced the agent while its item stayed frozen on the spawning run.

That is also why the reactivation predicate is separate from canRouteRelatedSubagent rather than a widening of it: an interrupted agent must not have its child thread pre-owned, but its identity is still resumable — the user stopped it, they didn't lose it.

Rehydration

Seeds turn ordinals alongside the registry (restarting at 1 would re-derive childRootNodeId for a different native turn) and parks non-terminal leftovers at idle, since a new session can never terminalize an activation it didn't drive and would otherwise keep the session pinned open.

Rehydration is covered directly. CodexAdapterV2.test.ts clears the process-local registry, seeds the next turn from projection-known subagents, and verifies that the same identity is reused with activation ordinal/count and lifetime usage advanced correctly. The recorded subagent_reuse_after_idle fixture remains unregistered while its broader replay path is investigated.

Testing

Typecheck clean across server, contracts, client-runtime, web. Server 1,684 passing.

Reviewed by an independent agent pass, which found the item/started / item/completed bug above plus the child-thread adoption hole, the row/item routing asymmetry, and both rehydration gaps. All fixed here.

Note

Attribute reused subagents to the run reactivating them in orchestration-v2

  • Rehydrates previously spawned subagents at root-turn start in CodexAdapterV2 by rebuilding the process-local registry from existingSubagents and rebinding them to the active run on collab agent interactions.
  • Extends event routing in RunExecutionService to match subagent rows, activations, and turn items by identity (not just run/thread), so reused subagents are admitted and tracked correctly.
  • Adds canReactivateSubagent to allow reactivation of idle/interrupted subagents; failed/cancelled are excluded. Classifies idle as a terminal status for cascade/routing purposes.
  • Finalizing a run now also terminalizes any open subagent activations and clears currentActivationId via cascadeTerminalizeRunOwnedSubagents.
  • Adds test fixtures and assertions for the subagent_reuse_after_idle scenario; the fixture is scaffolded but not yet registered in the test suite due to a replay hang.
  • Behavioral Change: live status updates for a subagent are suppressed when currentActivationId is null, replacing the removed isSettledCodexSubagent check.
📊 Macroscope summarized 85fe8a2. 10 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted (Automatic summaries will resume when PR exits draft mode or review begins).

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 60b982bc-2590-4668-bcae-0f0a98af8553

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch subagent-obs/03-reuse

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 27, 2026
@shivamhwp
shivamhwp force-pushed the subagent-obs/02-adapters branch from 9296337 to 760a4c2 Compare July 27, 2026 23:03
@shivamhwp
shivamhwp force-pushed the subagent-obs/03-reuse branch from 5187d5b to cde2598 Compare July 27, 2026 23:03
Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts
Comment thread apps/server/scripts/record-codex-app-server-replay-fixture.ts
@shivamhwp
shivamhwp force-pushed the subagent-obs/02-adapters branch from 760a4c2 to f3666e9 Compare July 28, 2026 15:31
@shivamhwp
shivamhwp force-pushed the subagent-obs/03-reuse branch from cde2598 to fc5b293 Compare July 28, 2026 15:31
@shivamhwp
shivamhwp force-pushed the subagent-obs/02-adapters branch from f3666e9 to 2565ea0 Compare July 28, 2026 19:27
@shivamhwp
shivamhwp force-pushed the subagent-obs/03-reuse branch 2 times, most recently from 85fe8a2 to 795eb20 Compare July 28, 2026 19:46
Comment thread apps/server/src/orchestration-v2/ProviderTurnStartService.ts Outdated
shivamhwp and others added 2 commits July 29, 2026 01:52
… them

A subagent row keeps the run id it was spawned under. Routing admits a
row by run or by parent thread, and the parent thread is excluded by
construction, so a later run re-activating that subagent could only see
its updates while the spawning run's ingestion fiber happened to still be
alive. The Claude adapter already re-attributed on reopen for exactly
this reason; this brings the same treatment to routing and to Codex.

Reuse arrives on item/completed, not item/started: resumeAgent and
sendInput only ever complete, and registerSubagentThreads ignores them
because they are not spawnAgent. Hooking the rebind there is what makes
it fire — subagent_continue now asserts the subagent is attributed to the
run that drove it, where before it stayed pinned to the spawning run.

Routing gains a third test: subagent identity, seeded from the projection
at turn start. The child thread is adopted only while the agent is live,
because a settled row is also re-emitted by trailing traffic — a token
usage frame or a collab state sweep — and adopting on those would hand a
later run an interrupted agent's thread, readmitting the stale events
that post-interrupt recovery exists to exclude.

A subagent's row and its timeline item are emitted together, so both
route by identity; matching only the row advanced the agent while its
item stayed frozen on the spawning run.

The reactivation predicate is separate from canRouteRelatedSubagent
rather than a widening of it, for the same reason: an interrupted agent
must not have its child thread pre-owned, but its identity is still
resumable, because the user stopped it rather than losing it.

Rehydration seeds turn ordinals alongside the registry — restarting them
at 1 would re-derive childRootNodeId for a different native turn — and
parks non-terminal leftovers at idle, since a new session can never
terminalize an activation it did not drive and would otherwise keep the
session pinned open.

Not proven: rehydration itself. The fixture that would exercise it is
recorded but does not replay to completion, for reasons unrelated to
these changes, and is left unregistered with the open question written
up in fixtures/index.ts.
…estart

A fresh adapter is the post-restart state: the process-local registry is
empty, so a returning native child thread can only be recognised through
the identities the projection hands back in existingSubagents.

Drives a transcript with no spawn frame, seeds one idle subagent, and
asserts the returning thread reuses that identity, continues the
activation count and ordinal rather than restarting them, and keeps the
usage recorded before the restart. Paired with a negative control that
replays the same transcript unseeded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shivamhwp
shivamhwp force-pushed the subagent-obs/02-adapters branch from bc22aac to 2567faf Compare July 28, 2026 20:26
@shivamhwp
shivamhwp force-pushed the subagent-obs/03-reuse branch from 795eb20 to 64bf642 Compare July 28, 2026 20:26

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions: one issue found. Known tagged failures should be recovered with Effect.catchTags rather than catchIf + a schema predicate.

Posted via Macroscope — Effect Service Conventions

Comment on lines +436 to +438
Effect.catchIf(Schema.is(ProjectionStoreThreadNotFoundError), () =>
Effect.succeed(null),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catchIf with a schema predicate is being used only to recover the known tagged failure ProjectionStoreThreadNotFoundError. Per the service conventions, statically known tagged failures should be recovered with Effect.catchTags({ ... }) (even for a single tag); catchIf is reserved for genuinely structural predicates. This also matches the pervasive Effect.catchTags usage elsewhere in the codebase.

Suggested change
Effect.catchIf(Schema.is(ProjectionStoreThreadNotFoundError), () =>
Effect.succeed(null),
),
Effect.catchTags({
ProjectionStoreThreadNotFoundError: () => Effect.succeed(null),
}),

Posted via Macroscope — Effect Service Conventions

Co-Authored-By: Claude <noreply@anthropic.com>
@shivamhwp
shivamhwp force-pushed the subagent-obs/03-reuse branch from 32cbb3d to c63297e Compare July 29, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant